Skip to content

feat(Codecov): add storage solution #90693

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

adrianviquez
Copy link
Contributor

@adrianviquez adrianviquez commented Apr 30, 2025

This PR adds a context to the Codecov product to hold state and functionality to save/get data to/from the url and local storage. This context is used in the CodecovProvider that will be the main wrapper for all things Codecov. This is the preferred approach over this pr.

Notes

  • When I name things "Codecov" here, I'm referring it as a product and not as the company. So thinking of it as a "feature" or "product" such as "replay" or "issues". It might be unnecessary for some cases (like the persistence/url), but I think it makes sense for the provider/context pieces. Open to thoughts here!
  • CodecovProvider: this is the main code that will act as a wrapper for everything in the Codecov product. It instantiates and initializes the CodecovContext component, as well as sets functions to manipulate the context.
  • CodecovContext: the context itself.

Thanks!

@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Apr 30, 2025
@adrianviquez adrianviquez requested a review from JonasBa April 30, 2025 20:09
@adrianviquez adrianviquez changed the title Adrian/add storage solution feat(Codecov): add storage solution May 5, 2025
@adrianviquez adrianviquez marked this pull request as ready for review May 5, 2025 19:56
@adrianviquez adrianviquez requested a review from a team as a code owner May 5, 2025 19:56
Copy link

codecov bot commented May 5, 2025

❌ 62 Tests Failed:

Tests completed Failed Passed Skipped
10218 62 10156 9
View the top 3 failed test(s) by shortest run time
Customer Details test vercel api endpoints does not render if subscription is not self serve partner
Stack Traces | 0.085s run time
TypeError: Cannot read properties of undefined (reading 'showInternalStats')
    at .../components/customers/customerStatsFilters.tsx:105:115
    at Array.filter (<anonymous>)
    at CustomerStatsFilters (.../components/customers/customerStatsFilters.tsx:105:62)
    at Object.react-stack-bottom-frame (.../react-dom/cjs/react-dom-client.development.js:23863:20)
    at renderWithHooks (.../react-dom/cjs/react-dom-client.development.js:5529:22)
    at updateFunctionComponent (.../react-dom/cjs/react-dom-client.development.js:8897:19)
    at beginWork (.../react-dom/cjs/react-dom-client.development.js:10522:18)
    at runWithFiberInDEV (.../react-dom/cjs/react-dom-client.development.js:1522:13)
    at performUnitOfWork (.../react-dom/cjs/react-dom-client.development.js:15140:22)
    at workLoopSync (.../react-dom/cjs/react-dom-client.development.js:14956:41)
    at renderRootSync (.../react-dom/cjs/react-dom-client.development.js:14936:11)
    at performWorkOnRoot (.../react-dom/cjs/react-dom-client.development.js:14462:44)
    at performSyncWorkOnRoot (.../react-dom/cjs/react-dom-client.development.js:16231:7)
    at flushSyncWorkAcrossRoots_impl (.../react-dom/cjs/react-dom-client.development.js:16079:21)
    at flushSpawnedWork (.../react-dom/cjs/react-dom-client.development.js:15677:9)
    at commitRoot (.../react-dom/cjs/react-dom-client.development.js:15403:9)
    at performWorkOnRoot (.../react-dom/cjs/react-dom-client.development.js:14526:15)
    at performWorkOnRootViaSchedulerTask (.../react-dom/cjs/react-dom-client.development.js:16216:7)
    at flushActQueue (.../react/cjs/react.development.js:566:34)
    at Object.<anonymous>.process.env.NODE_ENV.exports.act (.../react/cjs/react.development.js:859:10)
    at .../sentry/sentry/node_modules/@.../react/dist/act-compat.js:47:25
    at renderRoot (.../sentry/sentry/node_modules/@.../react/dist/pure.js:188:26)
    at Object.render (.../sentry/sentry/node_modules/@.../react/dist/pure.js:287:10)
    at render (.../js/sentry-test/reactTestingLibrary.tsx:291:28)
    at Object.<anonymous> (.../gsAdmin/views/customerDetails.spec.tsx:2344:39)
    at Promise.then.completed (.../sentry/sentry/node_modules/jest-circus/build/utils.js:298:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (.../sentry/sentry/node_modules/jest-circus/build/utils.js:231:10)
    at _callCircusTest (.../sentry/sentry/node_modules/jest-circus/build/run.js:316:40)
    at processTicksAndRejections (node:internal/process/task_queues:105:5)
    at _runTest (.../sentry/sentry/node_modules/jest-circus/build/run.js:252:3)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:126:9)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:121:9)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:121:9)
    at run (.../sentry/sentry/node_modules/jest-circus/build/run.js:71:3)
    at runAndTransformResultsToJestFormat (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)
    at jestAdapter (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)
    at runTestInternal (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:367:16)
    at runTest (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:444:34)
    at Object.worker (.../sentry/sentry/node_modules/jest-runner/build/testWorker.js:106:12)
Customer Details AddGiftEventsAction can gift events - ERRORS
Stack Traces | 0.086s run time
TypeError: Cannot read properties of undefined (reading 'showInternalStats')
    at .../components/customers/customerStatsFilters.tsx:105:115
    at Array.filter (<anonymous>)
    at CustomerStatsFilters (.../components/customers/customerStatsFilters.tsx:105:62)
    at Object.react-stack-bottom-frame (.../react-dom/cjs/react-dom-client.development.js:23863:20)
    at renderWithHooks (.../react-dom/cjs/react-dom-client.development.js:5529:22)
    at updateFunctionComponent (.../react-dom/cjs/react-dom-client.development.js:8897:19)
    at beginWork (.../react-dom/cjs/react-dom-client.development.js:10522:18)
    at runWithFiberInDEV (.../react-dom/cjs/react-dom-client.development.js:1522:13)
    at performUnitOfWork (.../react-dom/cjs/react-dom-client.development.js:15140:22)
    at workLoopSync (.../react-dom/cjs/react-dom-client.development.js:14956:41)
    at renderRootSync (.../react-dom/cjs/react-dom-client.development.js:14936:11)
    at performWorkOnRoot (.../react-dom/cjs/react-dom-client.development.js:14462:44)
    at performSyncWorkOnRoot (.../react-dom/cjs/react-dom-client.development.js:16231:7)
    at flushSyncWorkAcrossRoots_impl (.../react-dom/cjs/react-dom-client.development.js:16079:21)
    at flushSpawnedWork (.../react-dom/cjs/react-dom-client.development.js:15677:9)
    at commitRoot (.../react-dom/cjs/react-dom-client.development.js:15403:9)
    at performWorkOnRoot (.../react-dom/cjs/react-dom-client.development.js:14526:15)
    at performWorkOnRootViaSchedulerTask (.../react-dom/cjs/react-dom-client.development.js:16216:7)
    at flushActQueue (.../react/cjs/react.development.js:566:34)
    at Object.<anonymous>.process.env.NODE_ENV.exports.act (.../react/cjs/react.development.js:859:10)
    at .../sentry/sentry/node_modules/@.../react/dist/act-compat.js:47:25
    at renderRoot (.../sentry/sentry/node_modules/@.../react/dist/pure.js:188:26)
    at Object.render (.../sentry/sentry/node_modules/@.../react/dist/pure.js:287:10)
    at render (.../js/sentry-test/reactTestingLibrary.tsx:291:28)
    at Object.<anonymous> (.../gsAdmin/views/customerDetails.spec.tsx:3130:39)
    at Promise.then.completed (.../sentry/sentry/node_modules/jest-circus/build/utils.js:298:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (.../sentry/sentry/node_modules/jest-circus/build/utils.js:231:10)
    at _callCircusTest (.../sentry/sentry/node_modules/jest-circus/build/run.js:316:40)
    at processTicksAndRejections (node:internal/process/task_queues:105:5)
    at _runTest (.../sentry/sentry/node_modules/jest-circus/build/run.js:252:3)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:126:9)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:121:9)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:121:9)
    at run (.../sentry/sentry/node_modules/jest-circus/build/run.js:71:3)
    at runAndTransformResultsToJestFormat (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)
    at jestAdapter (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)
    at runTestInternal (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:367:16)
    at runTest (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:444:34)
    at Object.worker (.../sentry/sentry/node_modules/jest-runner/build/testWorker.js:106:12)
Customer Details allow grace period disabled in the dropdown
Stack Traces | 0.091s run time
TypeError: Cannot read properties of undefined (reading 'showInternalStats')
    at .../components/customers/customerStatsFilters.tsx:105:115
    at Array.filter (<anonymous>)
    at CustomerStatsFilters (.../components/customers/customerStatsFilters.tsx:105:62)
    at Object.react-stack-bottom-frame (.../react-dom/cjs/react-dom-client.development.js:23863:20)
    at renderWithHooks (.../react-dom/cjs/react-dom-client.development.js:5529:22)
    at updateFunctionComponent (.../react-dom/cjs/react-dom-client.development.js:8897:19)
    at beginWork (.../react-dom/cjs/react-dom-client.development.js:10522:18)
    at runWithFiberInDEV (.../react-dom/cjs/react-dom-client.development.js:1522:13)
    at performUnitOfWork (.../react-dom/cjs/react-dom-client.development.js:15140:22)
    at workLoopSync (.../react-dom/cjs/react-dom-client.development.js:14956:41)
    at renderRootSync (.../react-dom/cjs/react-dom-client.development.js:14936:11)
    at performWorkOnRoot (.../react-dom/cjs/react-dom-client.development.js:14462:44)
    at performSyncWorkOnRoot (.../react-dom/cjs/react-dom-client.development.js:16231:7)
    at flushSyncWorkAcrossRoots_impl (.../react-dom/cjs/react-dom-client.development.js:16079:21)
    at flushSpawnedWork (.../react-dom/cjs/react-dom-client.development.js:15677:9)
    at commitRoot (.../react-dom/cjs/react-dom-client.development.js:15403:9)
    at performWorkOnRoot (.../react-dom/cjs/react-dom-client.development.js:14526:15)
    at performWorkOnRootViaSchedulerTask (.../react-dom/cjs/react-dom-client.development.js:16216:7)
    at flushActQueue (.../react/cjs/react.development.js:566:34)
    at Object.<anonymous>.process.env.NODE_ENV.exports.act (.../react/cjs/react.development.js:859:10)
    at .../sentry/sentry/node_modules/@.../react/dist/act-compat.js:47:25
    at renderRoot (.../sentry/sentry/node_modules/@.../react/dist/pure.js:188:26)
    at Object.render (.../sentry/sentry/node_modules/@.../react/dist/pure.js:287:10)
    at render (.../js/sentry-test/reactTestingLibrary.tsx:291:28)
    at Object.<anonymous> (.../gsAdmin/views/customerDetails.spec.tsx:2055:39)
    at Promise.then.completed (.../sentry/sentry/node_modules/jest-circus/build/utils.js:298:28)
    at new Promise (<anonymous>)
    at callAsyncCircusFn (.../sentry/sentry/node_modules/jest-circus/build/utils.js:231:10)
    at _callCircusTest (.../sentry/sentry/node_modules/jest-circus/build/run.js:316:40)
    at processTicksAndRejections (node:internal/process/task_queues:105:5)
    at _runTest (.../sentry/sentry/node_modules/jest-circus/build/run.js:252:3)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:126:9)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:121:9)
    at _runTestsForDescribeBlock (.../sentry/sentry/node_modules/jest-circus/build/run.js:121:9)
    at run (.../sentry/sentry/node_modules/jest-circus/build/run.js:71:3)
    at runAndTransformResultsToJestFormat (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)
    at jestAdapter (.../sentry/sentry/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:79:19)
    at runTestInternal (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:367:16)
    at runTest (.../sentry/sentry/node_modules/jest-runner/build/runTest.js:444:34)
    at Object.worker (.../sentry/sentry/node_modules/jest-runner/build/testWorker.js:106:12)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';

// Types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Types should explain why, not what :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will adjust all comments

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't have been checked in

Comment on lines 16 to 19
export type CodecovContextSetterTypes = {
setContextState: React.Dispatch<React.SetStateAction<CodecovContextTypes>>;
updateSelectorData: (value: Partial<CodecovContextTypes>) => void;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export type CodecovContextSetterTypes = {
setContextState: React.Dispatch<React.SetStateAction<CodecovContextTypes>>;
updateSelectorData: (value: Partial<CodecovContextTypes>) => void;
};
export type CodecovContextSetterTypes = {
setContextState: React.Dispatch<React.SetStateAction<CodecovContextTypes>>;
updateSelectorData: (value: Partial<CodecovContextTypes>) => void;
};

Why do you need both here? If someone calls setContextState, it means it wont end up updating query params, potentially detaching state.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah true, I don't need to expose the setContextState fn, that should be private to the context itself. Left it there from the original implementation but no need to have it

import useOrganization from 'sentry/utils/useOrganization';

// Types
export type CodecovContextTypes = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export type CodecovContextTypes = {
export type CodecovContext = {

There is no need to annotate this any further

integratedOrg: string | null;
repository: string | null;
};
export type CodecovContextSetterTypes = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you export these separately?

Comment on lines 3 to 6
import type {
CodecovContextSetterTypes,
CodecovContextTypes,
} from 'sentry/components/codecov/container/container';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should only export a single type.

Also, codecov/container/container isn't good file structure (same with context/context).

It should be something like CodecovParamsProvider.tsx or similar to better explain what it does. The same goes for the provider, it may be obvious to you that CodecovProvider provides params, but to someone reading it for the first time, they'll have no idea what a provider like that does.

Copy link
Contributor Author

@adrianviquez adrianviquez May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed these to codecovParamsProvider.tsx and codecovContext.tsx. I normally let the folder help me with understanding it, so like codecov/components/context helps me understand it's a codecovContext, but it is true it can be clearer to just name the file codecovSomething


export function useCodecovContext() {
const context = useContext(CodecovContext);
if (!context) throw new Error('useCodecovContext must be used within CodecovProvider');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!context) throw new Error('useCodecovContext must be used within CodecovProvider');
if (context === undefined) throw new Error('useCodecovContext was called outside of CodecovProvider');

This is a nit, but I prefer to target the exact type here.

Wrt err messages, it's always better to explain why the error was thrown, and be precise about what caused it (your message has a more info tone to it)

Comment on lines 80 to 95
const updateSelectorData: CodecovContextSetterTypes['updateSelectorData'] = data => {
return setContextState(prev => {
const newState = {...prev, ...data};
setSelectorData(newState);

const currentParams = new URLSearchParams(location.search);
for (const [key, value] of Object.entries(newState)) {
if (value !== null) {
currentParams.set(key, value);
}
}
navigate(`${location.pathname}?${currentParams.toString()}`, {replace: true});

return newState;
});
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should useCallback here as this is stable. If something updates query params that you don't care about, the rerender will create a new function here and propagate updates to the tree.


return (
<CodecovContext.Provider
value={{...contextState, setContextState, updateSelectorData}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just as info, but a common practice in react to avoid this and separate the data into a getter and a setter is to separate it to a separate context.

Comment on lines 55 to 77
const [contextState, setContextState] = useState(() => {
// Get query params
const resultsFromQuery = CODECOV_URL_PARAM.reduce<Record<string, string | null>>(
(acc, value: string) => {
const queryParam = queryParams[value];
acc[value] =
typeof queryParam === 'string' ? decodeURIComponent(queryParam).trim() : null;
return acc;
},
{}
);
const hasAnyNonNull = Object.values(resultsFromQuery).some(value => value !== null);
if (hasAnyNonNull) {
return {...DEFAULT_CODECOV_CONTEXT_VALUES, ...resultsFromQuery};
}

// Get data from local storage
if (selectorData) {
return {...DEFAULT_CODECOV_CONTEXT_VALUES, ...selectorData};
}

return DEFAULT_CODECOV_CONTEXT_VALUES;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach here is brittle, as it means that if someone updates a query param of codecov directly without updateSelectorData, the update wont propagate to this provider and you'll end up with stale values. Imo, this is not what you want, and you should instead encourage people to write directly to query params and treat that as the source of truth (as opposed to state). Since URL is state, why do you need the separate react state for it?

@JonasBa
Copy link
Member

JonasBa commented May 5, 2025

@adrianviquez I would suggest to simplify this to not use React state and be resilient to query param updates outside of the provider.

function CodecovQueryParamsProvider(props){
  const location = useLocation();
  const localStorageState = useLocalStorage();

  const params = {
    key: location.query.key || localStorageState.key || default value
    ...
  }

  return <Provider value={params}>{props.children}</Provider>
}

The only part left then is to decide how to sync query to localStorage, which you could probably just do from inside an effect inside this provider

@adrianviquez
Copy link
Contributor Author

Ty for the feedback @JonasBa. I refactored the component to encourage using url as the state. I believe I addressed all of your suggestions, apologies if I missed one.

I created a updateSelectorData function that standardizes how to update the url. I believed your original suggestion was for each component to be responsible for this logic, but I thought standardizing how to do it in this class made sense. I'm flexible about this though if you think the reusability value isn't too high - I also noticed after the fact you said exposing data and setters tends to be a react anti-pattern, so open to adjusting this.

Please let me know what you think about these changes, thanks for all the care and feedback 🙇

);
const navigate = useNavigate();

const updateSelectorData: CodecovContextData['updateSelectorData'] = useCallback(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on this?

My intent is having one piece of logic that children using this provider can point to, so I created this fn. That being said, it currently only adds the 1st key/value of data (which is a single Record<string, string> though). So if I wanted to make it more robust, I could loop through the keys of data and add any value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Scope: Frontend Automatically applied to PRs that change frontend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants